(ns scripts.cljmon.logger
  (:use util.file
        util.utils)
  (:use clojure.tools.logging)
  (:require scripts.cljmon.csv
            scripts.cljmon.json)
  (:require clojure.string
            clojure.walk))

;;; recording

(def default-log "cljmon.log")


(def ^:dynamic *logfile* default-log)
(def ^:dynamic *format* :csv)
(def ^:dynamic *backup* false)
(def ^:dynamic *rollover* false)

(def ^:dynamic *print-screen* false)

(def ^:dynamic *rollover-flag* nil)
(def ^:dynamic *caching-headings* false)
(def ^:dynamic *cached-headings* nil) ; save heading lines so they can be repeated for rollover
(def ^:dynamic *output-buffer* nil) ; save output when failed to flush (e.g. file is being copied)

;;; range parsing
;;; XXX: it looks like some of the code should be moved to parser...

(defn- start-of-a-date [date]
  (let [calendar (java.util.Calendar/getInstance)]
    (doto calendar
      (.setTime date)
      (.set java.util.Calendar/HOUR_OF_DAY 0)
      (.set java.util.Calendar/MINUTE 0)
      (.set java.util.Calendar/SECOND 0)
      (.set java.util.Calendar/MILLISECOND 0))
    (.getTime (.getTime calendar))))

(defn- end-of-a-date [date]
  (+ (start-of-a-date date) (* 24 60 60 1000) -1))

(defn- range-of-a-date [date]
  [(start-of-a-date date) (end-of-a-date date)])

(defn- range-of-today []
  (range-of-a-date (now)))

(defn- range-of-yesterday []
  (range-of-a-date (java.util.Date. (- (.getTime (now)) (* 24 60 60 1000)))))

(defn- compute-range-only [range]
  (let [range (.toLowerCase range)]
    (cond
      (= range "auto") (keyword range)
      (= range "all") (keyword range)
      (= range "today") (range-of-today)
      (= range "yesterday") (range-of-yesterday)
      (re-matches #"\d\d\d\d-\d\d-\d\d" range) (range-of-a-date (parse-date range))
      :else (throw (Exception. (str "Invalid range: " range))))))

(defn- compute-range-from-only [from]
  (when from
    (let [from (.toLowerCase from)]
      (cond
        (re-matches #"\d\d\d\d-\d\d-\d\d" from) (start-of-a-date (parse-date from))
        (re-matches #"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" from) (.getDate (parse-datetime from))
        :else (throw (Exception. (str "Invalid --range-from: " from)))))))

(defn- compute-range-to-only [to]
  (when to
    (let [to (.toLowerCase to)]
      (cond
        (re-matches #"\d\d\d\d-\d\d-\d\d" to) (end-of-a-date (parse-date to))
        (re-matches #"\d\d\d\d-\d\d-\d\d \d\d:\d\d:\d\d" to) (+ (.getDate (parse-datetime to)) 999)
        :else (throw (Exception. (str "Invalid --range-to: " to)))))))

(defn compute-range [range from to]
  {:pre []}
  ;  (keyword (.toLowerCase range))
  (let [range (compute-range-only range)
        from (compute-range-from-only from)
        to (compute-range-to-only to)]
    (assert (or (not from) (not to) (not (vector? range))))
    (cond
      (vector? range) range
      (and from to) [from to]
      from [from Long/MAX_VALUE]
      to [0 to]
      :else range)))

;;;

(defn- backup-log [path]
  (let [dir (or (file-dirname path) ".")
        name (file-basename path)
        pattern (re-pattern (str (re-escape-meta name) "\\.(\\d+)"))
        backup-numbers (cons 0 (map #(Integer/parseInt %) (keep #(second (re-matches pattern %)) (file-list dir))))
        new-name (str name "." (inc (apply max backup-numbers)))]
    ;    (info "Renaming" path "to" new-name)
    (file-rename path new-name)))

(defn log-clear []
  (when *logfile*
    (when-not (instance? StringBuilder *logfile*)
      (when (and *backup* (file-exists? *logfile*))
        (backup-log *logfile*))
      (file-clear *logfile*))))

(defn- rollover-log [path]
  (let [flag @*rollover-flag*]
    (if-not flag
      (reset! *rollover-flag* (start-of-a-date (now)))
      (when (< (+ flag (* 24 60 60 1000)) (System/currentTimeMillis))
        (swap! *rollover-flag* + (* 24 60 60 1000))
        (backup-log *logfile*)
        (file-save @*cached-headings* *logfile*)))))

(defn- buffered-output [file content]
  (if *output-buffer*
    (let [content (swap! *output-buffer* str content)]
      (try
        (file-append content file)
        (reset! *output-buffer* "")
        (catch Exception e
          (error e "Log error (probably), will retry next time."))))
    (file-append content file)))

(defn- log-line [line]
  (when line
    (when *print-screen*
      (println line))
    (when *logfile*
      (if (instance? StringBuilder *logfile*)
        (.append *logfile* (str line line-separator))
        (do
          (when *rollover*
            (if *caching-headings*
              (swap! *cached-headings* str line line-separator)
              (rollover-log *logfile*)))
          (buffered-output *logfile* (str line line-separator)))))))

(defn- format-field [field]
  (cond
    (float? field) (format "%.2f" field)
    :else field))

(defn log-records [data]
  (let [data (clojure.walk/postwalk format-field data)] ; TODO: do not loss precision in json mode
    (log-line ((case *format* :csv scripts.cljmon.csv/format-records :json scripts.cljmon.json/format-records) data))))

(defn log-headers [fields]
  (log-line ((case *format* :csv scripts.cljmon.csv/format-headers :json scripts.cljmon.json/format-headers) fields)))

(defn log-comments [comments]
  (log-line ((case *format* :csv scripts.cljmon.csv/format-comments :json scripts.cljmon.json/format-comments) comments)))

(defn log-name []
  (cond
    (instance? StringBuilder *logfile*) "<StringBuilder>"
    *logfile* *logfile*
    :else "stdout"))


(defn log-comments-and-headers [comments headers]
  (binding [*caching-headings* true]
    ;; log process info in comments
    (doseq [comment comments]
        (log-comments comment))
    (log-comments "")
    ;; headers
    (log-headers headers)))
